{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2.0.0\n"
     ]
    }
   ],
   "source": [
    "import tensorflow as tf\n",
    "print(tf.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 卷积神经网络（LeNet）\n",
    "\n",
    "在[“多层感知机的从零开始实现”](../chapter_deep-learning-basics/mlp-scratch.ipynb)一节里我们构造了一个含单隐藏层的多层感知机模型来对Fashion-MNIST数据集中的图像进行分类。每张图像高和宽均是28像素。我们将图像中的像素逐行展开，得到长度为784的向量，并输入进全连接层中。然而，这种分类方法有一定的局限性。\n",
    "\n",
    "1. 图像在同一列邻近的像素在这个向量中可能相距较远。它们构成的模式可能难以被模型识别。\n",
    "2. 对于大尺寸的输入图像，使用全连接层容易导致模型过大。假设输入是高和宽均为$1,000$像素的彩色照片（含3个通道）。即使全连接层输出个数仍是256，该层权重参数的形状也是$3,000,000\\times 256$：它占用了大约3 GB的内存或显存。这会带来过于复杂的模型和过高的存储开销。\n",
    "\n",
    "卷积层尝试解决这两个问题。一方面，卷积层保留输入形状，使图像的像素在高和宽两个方向上的相关性均可能被有效识别；另一方面，卷积层通过滑动窗口将同一卷积核与不同位置的输入重复计算，从而避免参数尺寸过大。\n",
    "\n",
    "卷积神经网络就是含卷积层的网络。本节里我们将介绍一个早期用来识别手写数字图像的卷积神经网络：LeNet [1]。这个名字来源于LeNet论文的第一作者Yann LeCun。LeNet展示了通过梯度下降训练卷积神经网络可以达到手写数字识别在当时最先进的结果。这个奠基性的工作第一次将卷积神经网络推上舞台，为世人所知。\n",
    "\n",
    "## 5.5.1 LeNet模型\n",
    "\n",
    "LeNet分为卷积层块和全连接层块两个部分。下面我们分别介绍这两个模块。\n",
    "\n",
    "卷积层块里的基本单位是卷积层后接最大池化层：卷积层用来识别图像里的空间模式，如线条和物体局部，之后的最大池化层则用来降低卷积层对位置的敏感性。卷积层块由两个这样的基本单位重复堆叠构成。在卷积层块中，每个卷积层都使用$5\\times 5$的窗口，并在输出上使用sigmoid激活函数。第一个卷积层输出通道数为6，第二个卷积层输出通道数则增加到16。这是因为第二个卷积层比第一个卷积层的输入的高和宽要小，所以增加输出通道使两个卷积层的参数尺寸类似。卷积层块的两个最大池化层的窗口形状均为$2\\times 2$，且步幅为2。由于池化窗口与步幅形状相同，池化窗口在输入上每次滑动所覆盖的区域互不重叠。\n",
    "\n",
    "卷积层块的输出形状为(批量大小, 通道, 高, 宽)。当卷积层块的输出传入全连接层块时，全连接层块会将小批量中每个样本变平（flatten）。也就是说，全连接层的输入形状将变成二维，其中第一维是小批量中的样本，第二维是每个样本变平后的向量表示，且向量长度为通道、高和宽的乘积。全连接层块含3个全连接层。它们的输出个数分别是120、84和10，其中10为输出的类别个数。\n",
    "\n",
    "下面我们通过`Sequential`类来实现LeNet模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "net = tf.keras.models.Sequential([\n",
    "    tf.keras.layers.Conv2D(filters=6,kernel_size=5,activation='sigmoid',input_shape=(28,28,1)),\n",
    "    tf.keras.layers.MaxPool2D(pool_size=2, strides=2),\n",
    "    tf.keras.layers.Conv2D(filters=16,kernel_size=5,activation='sigmoid'),\n",
    "    tf.keras.layers.MaxPool2D(pool_size=2, strides=2),\n",
    "    tf.keras.layers.Flatten(),\n",
    "    tf.keras.layers.Dense(120,activation='sigmoid'),\n",
    "    tf.keras.layers.Dense(84,activation='sigmoid'),\n",
    "    tf.keras.layers.Dense(10,activation='sigmoid')\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来我们构造一个高和宽均为28的单通道数据样本，并逐层进行前向计算来查看每个层的输出形状。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "conv2d output shape\t (1, 24, 24, 6)\n",
      "max_pooling2d output shape\t (1, 12, 12, 6)\n",
      "conv2d_1 output shape\t (1, 8, 8, 16)\n",
      "max_pooling2d_1 output shape\t (1, 4, 4, 16)\n",
      "flatten output shape\t (1, 256)\n",
      "dense output shape\t (1, 120)\n",
      "dense_1 output shape\t (1, 84)\n",
      "dense_2 output shape\t (1, 10)\n"
     ]
    }
   ],
   "source": [
    "X = tf.random.uniform((1,28,28,1))\n",
    "for layer in net.layers:\n",
    "    X = layer(X)\n",
    "    print(layer.name, 'output shape\\t', X.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，在卷积层块中输入的高和宽在逐层减小。卷积层由于使用高和宽均为5的卷积核，从而将高和宽分别减小4，而池化层则将高和宽减半，但通道数则从1增加到16。全连接层则逐层减少输出个数，直到变成图像的类别数10。\n",
    "\n",
    "\n",
    "## 5.5.2 获取数据和训练模型\n",
    "\n",
    "下面我们来实验LeNet模型。实验中，我们仍然使用Fashion-MNIST作为训练数据集。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "fashion_mnist = tf.keras.datasets.fashion_mnist\n",
    "\n",
    "(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(60000, 28, 28, 1)\n"
     ]
    }
   ],
   "source": [
    "train_images = tf.reshape(train_images, (train_images.shape[0],train_images.shape[1],train_images.shape[2], 1))\n",
    "print(train_images.shape)\n",
    "\n",
    "test_images = tf.reshape(test_images, (test_images.shape[0],test_images.shape[1],test_images.shape[2], 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "损失函数和训练算法依然采用交叉熵损失函数(cross entropy)和小批量随机梯度下降(SGD)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = tf.keras.optimizers.SGD(learning_rate=0.9, momentum=0.0, nesterov=False)\n",
    "\n",
    "net.compile(optimizer=optimizer,\n",
    "              loss='sparse_categorical_crossentropy',\n",
    "              metrics=['accuracy'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Train on 54000 samples, validate on 6000 samples\n",
      "Epoch 1/5\n",
      "54000/54000 [==============================] - 13s 238us/sample - loss: 1.4932 - accuracy: 0.4121 - val_loss: 0.8236 - val_accuracy: 0.6315\n",
      "Epoch 2/5\n",
      "54000/54000 [==============================] - 12s 229us/sample - loss: 0.6498 - accuracy: 0.7406 - val_loss: 0.5940 - val_accuracy: 0.7680\n",
      "Epoch 3/5\n",
      "54000/54000 [==============================] - 12s 221us/sample - loss: 0.5609 - accuracy: 0.7768 - val_loss: 0.5362 - val_accuracy: 0.7817\n",
      "Epoch 4/5\n",
      "54000/54000 [==============================] - 13s 233us/sample - loss: 0.5213 - accuracy: 0.7965 - val_loss: 0.5622 - val_accuracy: 0.7907\n",
      "Epoch 5/5\n",
      "54000/54000 [==============================] - 12s 225us/sample - loss: 0.4913 - accuracy: 0.8093 - val_loss: 0.5753 - val_accuracy: 0.7737\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<tensorflow.python.keras.callbacks.History at 0x1a5dc70ce48>"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "net.fit(train_images, train_labels, epochs=5, validation_split=0.1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10000/1 - 1s - loss: 0.5165 - accuracy: 0.7527\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[0.6210818708896637, 0.7527]"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "net.evaluate(test_images, test_labels, verbose=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 小结\n",
    "\n",
    "* 卷积神经网络就是含卷积层的网络。\n",
    "* LeNet交替使用卷积层和最大池化层后接全连接层来进行图像分类。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "tf2.0",
   "language": "python",
   "name": "tf2.0"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
